# src/builder.py
import subprocess
import json
import re
import shlex
import os
from typing import List

def build_image(context_dir, dockerfile_path, image_name, builder="docker"):
    """调用 docker/buildah 构建镜像"""
    if builder == "docker":
        cmd = ["docker", "build", "-t", image_name, "-f", dockerfile_path, context_dir]
    else:
        cmd = ["buildah", "bud", "-t", image_name, "-f", dockerfile_path, context_dir]
    subprocess.run(cmd, check=True)


def export_image(image_name: str, export_path: str) -> bool:
    """
    导出 docker 镜像为 tar 文件
    
    Args:
        image_name: 镜像名称（如 myapp:latest）
        export_path: 导出文件路径（如 ./myapp.tar）
    
    Returns:
        bool: 导出是否成功
    """
    try:
        # 确保导出目录存在
        export_dir = os.path.dirname(os.path.abspath(export_path))
        if export_dir and not os.path.exists(export_dir):
            os.makedirs(export_dir, exist_ok=True)
        
        # 使用 docker save 命令导出镜像
        cmd = ["docker", "save", "-o", export_path, image_name]
        
        print(f"   执行命令: {' '.join(cmd)}")
        result = subprocess.run(cmd, check=True, capture_output=True, text=True)
        
        # 检查文件是否成功创建
        if os.path.exists(export_path) and os.path.getsize(export_path) > 0:
            file_size = os.path.getsize(export_path)
            size_mb = file_size / (1024 * 1024)
            print(f"   镜像文件大小: {size_mb:.2f} MB")
            return True
        else:
            print(f"   错误: 导出文件未创建或大小为 0")
            return False
            
    except subprocess.CalledProcessError as e:
        print(f"   Docker save 命令执行失败: {e}")
        if e.stderr:
            print(f"   错误详情: {e.stderr}")
        return False
    except Exception as e:
        print(f"   导出过程中发生错误: {e}")
        return False


def get_image_info(image_name: str) -> dict:
    """
    获取镜像基本信息（可选功能，用于验证镜像存在性）
    
    Args:
        image_name: 镜像名称
        
    Returns:
        dict: 镜像信息，包含 size, created 等字段
    """
    try:
        # 使用 docker inspect 获取镜像信息
        cmd = ["docker", "inspect", image_name]
        result = subprocess.run(cmd, check=True, capture_output=True, text=True)
        
        inspect_data = json.loads(result.stdout)
        
        if inspect_data:
            image_data = inspect_data[0]
            return {
                "id": image_data.get("Id", "")[:12],  # 短ID
                "size": image_data.get("Size", 0),
                "created": image_data.get("Created", ""),
                "arch": image_data.get("Architecture", ""),
                "os": image_data.get("Os", "")
            }
        else:
            return {}
            
    except subprocess.CalledProcessError:
        # 镜像不存在或命令失败
        return {}
    except Exception as e:
        print(f"获取镜像信息时发生错误: {e}")
        return {}


def read_file_from_image(image_name: str, path: str) -> str:
    try:
        out = subprocess.check_output(
            ["docker", "run", "--rm", "--entrypoint", "/bin/cat", image_name, path],
            text=True,
        )
        return out
    except Exception:
        try:
            out = subprocess.check_output(
                ["docker", "run", "--rm", "--entrypoint", "/bin/sh", image_name, "-lc", f"cat {shlex.quote(path)} || true"],
                text=True,
            )
            return out
        except Exception:
            return ""


def read_ldd_from_image(image_name: str) -> str:
    return read_file_from_image(image_name, "/ldd.txt")


def _docker_run_strace(image: str, binary: str, argv: list[str], timeout: int) -> tuple[int, str, str]:
    """
    在容器内用 strace 执行 <binary> [argv]，返回 (exit_code, stdout, stderr(strace输出)).
    若没有 strace，则尝试在容器内临时安装后再执行。
    """
    base_cmd = ["docker", "run", "--rm", "--cap-add=SYS_PTRACE", "--entrypoint", "/bin/sh", image]
    # 1) 直接尝试运行（假设存在 strace）
    cmd1 = base_cmd + ["-lc", f"which strace >/dev/null 2>&1 && strace -f -qq -s 256 -e trace=file,network,process,openat,connect {shlex.quote(binary)} {' '.join(shlex.quote(a) for a in argv)} || exit 99"]
    try:
        out = subprocess.run(cmd1, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
        if out.returncode != 99:  # 99 表示没找到 strace
            return out.returncode, out.stdout, out.stderr
    except subprocess.TimeoutExpired as e:
        return 124, e.stdout or "", e.stderr or "timeout"

    # 2) 容器内临时安装 strace 再跑（需要网络）
    install = "if command -v microdnf >/dev/null 2>&1; then microdnf install -y strace >/dev/null; else dnf -y install strace >/dev/null; fi"
    cmd2 = base_cmd + ["-lc", f"{install} && strace -f -qq -s 256 -e trace=file,network,process,openat,connect {shlex.quote(binary)} {' '.join(shlex.quote(a) for a in argv)}"]
    try:
        out = subprocess.run(cmd2, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
        return out.returncode, out.stdout, out.stderr
    except subprocess.TimeoutExpired as e:
        return 124, e.stdout or "", e.stderr or "timeout"


_OPEN_RE = re.compile(r'openat\([^,]+,\s*"([^"]+)"[^)]*\)\s*=\s*(-?\d+)')
_STAT_RE = re.compile(r'(?:statx?|access)\([^,]+,\s*"([^"]+)"[^)]*\)\s*=\s*(-?\d+)')
_CONNECT_RE = re.compile(r'connect\([^,]+,\s*[^:]+:([^\)]+)\)\s*=\s*(-?\d+)')

def _parse_strace(stderr: str):
    files = set()
    files_ok = set()
    files_err = set()
    nets = set()

    for line in stderr.splitlines():
        line = line.strip()
        m = _OPEN_RE.search(line) or _STAT_RE.search(line)
        if m:
            path, rc = m.group(1), int(m.group(2))
            files.add(path)
            if rc >= 0:
                files_ok.add(path)
            else:
                files_err.add(path)
            continue
        m2 = _CONNECT_RE.search(line)
        if m2:
            dest = m2.group(1)
            nets.add(dest)

    return {
        "files_all": sorted(files),
        "files_ok": sorted(files_ok),
        "files_missing": sorted(p for p in files_err if p not in files_ok),
        "net": sorted(nets),
    }


def probe_runtime_deps(image_name: str, bin_paths: list[str], names: list[str], args_map: dict[str, list[str]]|None, timeout_sec: int = 20):
    """
    对每个二进制做一次探测（默认 argv=['--help']）。
    names: 与 bin_paths 对应的"友好名"（如入口末级）用于报告。
    args_map: {'name': ['--version']} 用于覆盖默认参数。
    """
    args_map = args_map or {}
    results = []
    errors = []

    for name, bp in zip(names, bin_paths):
        argv = args_map.get(name) or ["--help"]
        code, out, err = _docker_run_strace(image_name, bp, argv, timeout=timeout_sec)
        parsed = _parse_strace(err)
        results.append({
            "name": name,
            "binary": bp,
            "argv": argv,
            "exit_code": code,
            "stdout": out[-4000:],   # 截断，避免过大
            "stderr_tail": err[-4000:],
            **parsed
        })
        if code not in (0, 2):  # 很多 --help 返回 0 或 2（如 flag 解析）
            errors.append(f"{name} exited {code}")

    return {"bins": results, "errors": errors}


def write_runtime_dependencies(runtime_deps: List[str], dockerfile_path: str):
    """将动态探测的依赖包写入 Dockerfile"""
    with open(dockerfile_path, "a", encoding="utf-8") as f:
        f.write("\n# 运行时依赖包\n")
        for dep in runtime_deps:
            f.write(f"RUN microdnf install -y {dep} && microdnf clean all\n")


# 测试函数（可选）
def test_export_functionality():
    """测试导出功能（仅供开发时测试）"""
    test_image = "hello-world:latest"
    test_path = "./test_export.tar"
    
    # 先拉取测试镜像
    try:
        subprocess.run(["docker", "pull", test_image], check=True)
        print(f"✅ 测试镜像 {test_image} 准备就绪")
        
        # 测试导出
        if export_image(test_image, test_path):
            print(f"✅ 导出测试成功: {test_path}")
            # 清理测试文件
            if os.path.exists(test_path):
                os.remove(test_path)
                print("🧹 已清理测试文件")
        else:
            print("❌ 导出测试失败")
            
    except subprocess.CalledProcessError:
        print("❌ 无法拉取测试镜像，跳过导出功能测试")


if __name__ == "__main__":
    # 可以单独运行此文件进行测试
    test_export_functionality()